AWSでジョブWorkerを構成するベストプラクティス 〜 SQSの巻
よく訓練されたアップル信者、都元です。AWSでシステム構築をする場合は、Design for failureという考えに基いて、複数AZにまたがる形の冗長構成を組むのがベストプラクティスです。さらに、このように分散させた各インスタンスには、出来る限りマスターを作らない、つまりSPOFとなるインスタンスを避ける構成であるのが理想です。
という話題については以前AWSにおける可用性の考え方というエントリーでも書きました。
可用性 (availability) と拡張性 (scalability)
本題はジョブWorkerですが、WebサーバやDBサーバの可用性と拡張性を先におさらいしておきましょう。
Webサーバ
この考えで構築する最も基本的な構成が、Webシステムにおける ELB + Webサーバ の構成です。この構成マルチAZと呼び、片方のAZが丸ごとダウンしたとしても、サービス自体はダウンさせずに継続できる *1、という特徴があります。Webサーバ内に特別な「マスター」はありませんので、どちらのAZがダウンしても同じです。所謂 HA (high-availability) な構成です。
また、Webサーバを4台、6台、8台と増やしていくことによって、スケーラビリティが確保できます。
ちなみに、ELBについては、サービス自体がマルチAZで構成されていますので、この冗長性は特に意識せずとも確保されています。
DBサーバ
次にDBについて考えます。RDSを利用する場合、複数のAZにまたがってレプリケーションを行えます。
この場合、片方がマスター(稼動系)となり、もう片方がスレーブ(待機系)となります。従って、残念ながらこの場合は「マスターを作らない」という条件を満たすことができません。稼動系の方のAZが丸ごとダウンした場合、待機系のマスター昇格が発生し、フェイルオーバーによって一定の可用性を確保します。フェイルオーバーには数分の時間がかかりますので、完全なHA構成ではありません。
また、RDSは複数のマスターを作れませんので、処理能力の拡大は基本的にスケールアップによって行うことが多いです。読み込み負荷が大きいケースであれば、リードレプリカによってスケールアウトが可能ですが、その数にも一定の制限があります。というわけで、RDSはスケーラビリティについても限定的であるのが実情です。
DBに可用性と拡張性を確保したい場合は、リレーショナル・データベースを諦めて、DynamoDB等を使うことも検討します。DynamoDBは、マルチAZの仕組みで可用性と拡張性を確保しています。
ジョブWorker
さて、だいぶ前置きが長くなってしましました。ここから本題。
Webサーバは多くの場合、HTTPリクエストを受け取り、何らかの処理を行い、処理完了後にHTTPレスポンスを返す、という所謂「同期」処理を担います。一方、ジョブWorkerは、処理依頼(ジョブ)を受け付けて待ち行列(キュー)に追加し、ジョブの完了に関係なく制御が依頼元に戻る、という所謂「非同期」処理を担います。Workerは主にバッチ処理等に使われるアーキテクチャです。
例えば、大きめな集計処理や動画のエンコーディング処理等を行うWebサービスを考えます。これらの処理は、1件につき数十分〜数時間掛かるものだとイメージしてみてください。このような処理のリクエストを同期処理として実行してしまうと、実行ボタンをクリックした後に何らかのネットワーク切断が発生したら、全てを最初からやり直す羽目になるでしょう。まぁ、それ以前にHTTPのタイムアウトが発生しますよね。
こういう処理は非同期処理として実装するのが常套です。HTTPとしては「処理を受け付けました。進捗はこちらで確認できます。」的なレスポンスを即座に返し、進捗確認ページでは、現在のジョブのステータスを確認できる、といった形です。
このような仕組みは、前述の「ELB + Webサーバ」ではなく「SQS + Workerサーバ」という構成で実現します。
クライアント *2はSQSキューにジョブを登録します。Workerはキューに対してポーリングを行い、ジョブを認識し次第処理を行います。
SQS自体はELBと同様、可用性と拡張性はサービスとして確保されていますので、我々は気にする必要はありません。キューの出口で待つWorkerはマルチAZ分散していますので、AZダウンにも対応できます。また、特にマスターもないので可用性には問題ありません。キューに貯まるメッセージが多くなった場合は台数を増やして行けばいいので、スケーラビリティも充分に確保できています。
まとめ
全部組み合わせてみるとこんなかんじです。
Workerは、処理結果をDBに書き戻しても良いですし、S3に書き込んでもOK。EMRやElastic Transcoderをキックしたり、SESでメールを送っても構いません。それは実行する処理次第ですね。
最後に注意点を2つ。SQSが分散キューであることに起因する制限事項があります。
1つ目。キュー内のジョブの順序は保証されません。まぁ大抵、おおむね、enqueue(投入)した順にdequeue(出力)されます。が、これは保証されたものではありません。ジョブ同士の前後関係を制御したい場合は、「順序は保証されない」ということを意識して、ワーカー側に別途独自に何らかの実装が必要です。
2つ目。メッセージは「at least once delivery」です。何のことかと言うと、「1つのメッセージをenqueueしたら、最低1回はそのメッセージをdequeueします」ってことです。いいじゃん、普通じゃん、と思うわけですが。よく考えれば「1つのenqueueに対して、同じメッセージが2回dequeueされてしまうことがある」と言っているのです。まぁ大抵、おおむね、1つのenqueueメッセージは、1回だけdequeueされます。が、これは保証されたものではありません。ジョブ自体を冪等に実装する *3か、もしくは重複処理を防止する処理をワーカー側に仕込む必要があります。
という2点には、是非お気をつけて。